/*
 * SoapUI, Copyright (C) 2004-2017 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent 
 * versions of the EUPL (the "Licence"); 
 * You may not use this work except in compliance with the Licence. 
 * You may obtain a copy of the Licence at: 
 * 
 * http://ec.europa.eu/idabc/eupl 
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is 
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied. See the Licence for the specific language governing permissions and limitations 
 * under the Licence. 
 */

package com.eviware.soapui.impl.wsdl.teststeps.assertions.basic;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.TestAssertionConfig;
import com.eviware.soapui.impl.rest.RestResource;
import com.eviware.soapui.impl.rest.RestService;
import com.eviware.soapui.impl.support.AbstractInterface;
import com.eviware.soapui.impl.support.DefinitionContext;
import com.eviware.soapui.impl.wadl.WadlDefinitionContext;
import com.eviware.soapui.impl.wadl.support.WadlValidator;
import com.eviware.soapui.impl.wsdl.WsdlInterface;
import com.eviware.soapui.impl.wsdl.WsdlOperation;
import com.eviware.soapui.impl.wsdl.panels.assertions.AssertionCategoryMapping;
import com.eviware.soapui.impl.wsdl.panels.assertions.AssertionListEntry;
import com.eviware.soapui.impl.wsdl.submit.RestMessageExchange;
import com.eviware.soapui.impl.wsdl.submit.WsdlMessageExchange;
import com.eviware.soapui.impl.wsdl.support.PathUtils;
import com.eviware.soapui.impl.wsdl.support.soap.SoapVersion;
import com.eviware.soapui.impl.wsdl.support.wsdl.WsdlContext;
import com.eviware.soapui.impl.wsdl.support.wsdl.WsdlValidator;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlMessageAssertion;
import com.eviware.soapui.impl.wsdl.teststeps.assertions.AbstractTestAssertionFactory;
import com.eviware.soapui.model.TestPropertyHolder;
import com.eviware.soapui.model.iface.MessageExchange;
import com.eviware.soapui.model.iface.SubmitContext;
import com.eviware.soapui.model.propertyexpansion.PropertyExpansionContext;
import com.eviware.soapui.model.testsuite.Assertable;
import com.eviware.soapui.model.testsuite.AssertionError;
import com.eviware.soapui.model.testsuite.AssertionException;
import com.eviware.soapui.model.testsuite.RequestAssertion;
import com.eviware.soapui.model.testsuite.ResponseAssertion;
import com.eviware.soapui.model.testsuite.TestCaseRunContext;
import com.eviware.soapui.model.testsuite.TestCaseRunner;
import com.eviware.soapui.support.StringUtils;
import com.eviware.soapui.support.UISupport;
import com.eviware.soapui.support.xml.XmlObjectConfigurationBuilder;
import com.eviware.soapui.support.xml.XmlObjectConfigurationReader;
import org.apache.xmlbeans.XmlObject;

import java.util.HashMap;
import java.util.Map;

/**
 * Asserts that a request or response message complies with its related WSDL
 * definition / XML Schema
 *
 * @author Ole.Matzura
 */

public class SchemaComplianceAssertion extends WsdlMessageAssertion implements RequestAssertion, ResponseAssertion {
    public static final String ID = "Schema Compliance";
    public static final String LABEL = "Schema Compliance";

    private String definition;
    private DefinitionContext<?> definitionContext;
    private String wsdlContextDef;
    private static Map<String, WsdlContext> wsdlContextMap = new HashMap<String, WsdlContext>();
    private static final String SCHEMA_COMPLIANCE_HAS_CLEARED_CACHE_FLAG = SchemaComplianceAssertion.class.getName()
            + "@SchemaComplianceHasClearedCacheFlag";
    public static final String DESCRIPTION = "Validates that the last received message is compliant with the associated WSDL or WADL schema definition. Applicable to SOAP and REST TestSteps.";

    public SchemaComplianceAssertion(TestAssertionConfig assertionConfig, Assertable assertable) {
        super(assertionConfig, assertable, false, true, false, true);

        XmlObjectConfigurationReader reader = new XmlObjectConfigurationReader(getConfiguration());
        definition = reader.readString("definition", null);
    }

    @Override
    public void prepare(TestCaseRunner testRunner, TestCaseRunContext testRunContext) throws Exception {
        super.prepare(testRunner, testRunContext);

        definitionContext = null;
        wsdlContextDef = null;

        // get correct context for checking if cache has been cleared for this run
        PropertyExpansionContext context = testRunContext.hasProperty(TestCaseRunContext.LOAD_TEST_CONTEXT) ? (PropertyExpansionContext) testRunContext
                .getProperty(TestCaseRunContext.LOAD_TEST_CONTEXT) : testRunContext;

        synchronized (context) {
            if (!context.hasProperty(SCHEMA_COMPLIANCE_HAS_CLEARED_CACHE_FLAG)) {
                wsdlContextMap.clear();
                context.setProperty(SCHEMA_COMPLIANCE_HAS_CLEARED_CACHE_FLAG, "yep!");
            }
        }
    }

    protected String internalAssertResponse(MessageExchange messageExchange, SubmitContext context)
            throws AssertionException {
        if (messageExchange instanceof WsdlMessageExchange) {
            return assertWsdlResponse((WsdlMessageExchange) messageExchange, context);
        } else if (messageExchange instanceof RestMessageExchange) {
            return assertWadlResponse((RestMessageExchange) messageExchange, context);
        }

        throw new AssertionException(new AssertionError("Unknown MessageExchange type"));
    }

    @Override
    protected String internalAssertProperty(TestPropertyHolder source, String propertyName,
                                            MessageExchange messageExchange, SubmitContext context) throws AssertionException {
        return null;
    }

    private String assertWadlResponse(RestMessageExchange messageExchange, SubmitContext context)
            throws AssertionException {
        WadlDefinitionContext wadlContext = null;
        try {
            definitionContext = getWadlContext(messageExchange, context);
        } catch (Exception e1) {
            throw new AssertionException(new AssertionError(e1.getMessage()));
        }

        WadlValidator validator = new WadlValidator(wadlContext);

        try {
            AssertionError[] errors = validator.assertResponse(messageExchange);
            if (errors.length > 0) {
                throw new AssertionException(errors);
            }
        } catch (AssertionException e) {
            throw e;
        } catch (Exception e) {
            throw new AssertionException(new AssertionError(e.getMessage()));
        }

        return "Schema compliance OK";
    }

    private String assertWsdlResponse(WsdlMessageExchange messageExchange, SubmitContext context)
            throws AssertionException {
        WsdlContext wsdlContext;
        try {
            wsdlContext = (WsdlContext) getWsdlContext(messageExchange, context);
        } catch (Exception e1) {
            throw new AssertionException(new AssertionError(e1.getMessage()));
        }

        WsdlValidator validator = new WsdlValidator(wsdlContext);

        try {
            AssertionError[] errors = validator.assertResponse(messageExchange, false);
            if (errors.length > 0) {
                throw new AssertionException(errors);
            }
        } catch (AssertionException e) {
            throw e;
        } catch (Exception e) {
            throw new AssertionException(new AssertionError(e.getMessage()));
        }

        return "Schema compliance OK";
    }

    private DefinitionContext<?> getWsdlContext(WsdlMessageExchange messageExchange, SubmitContext context)
            throws Exception {
        WsdlOperation operation = messageExchange.getOperation();
        WsdlInterface iface = operation.getInterface();
        String def = PathUtils.expandPath(definition, iface, context);
        if (StringUtils.isNullOrEmpty(def) || def.equals(iface.getDefinition())) {
            definitionContext = (iface).getWsdlContext();
            definitionContext.loadIfNecessary();
        } else {
            if (definitionContext == null || !def.equals(wsdlContextDef)) {
                definitionContext = getContext(def, iface.getSoapVersion());
                // ( (WsdlContext) definitionContext ).load();
                ((WsdlContext) definitionContext).setInterface(iface);
                wsdlContextDef = def;
            }
        }

        return definitionContext;
    }

    private synchronized WsdlContext getContext(String wsdlLocation, SoapVersion soapVersion) throws Exception {
        if (wsdlContextMap.containsKey(wsdlLocation)) {
            return wsdlContextMap.get(wsdlLocation);
        } else {
            WsdlContext newWsdlContext = new WsdlContext(wsdlLocation, soapVersion);
            newWsdlContext.load();
            wsdlContextMap.put(wsdlLocation, newWsdlContext);
            return newWsdlContext;
        }
    }

    private DefinitionContext<?> getWadlContext(RestMessageExchange messageExchange, SubmitContext context)
            throws Exception {
        RestResource operation = messageExchange.getResource();
        RestService service = operation.getService();
        if (StringUtils.isNullOrEmpty(definition)
                || definition.equals(PathUtils.expandPath(service.getDefinition(), service, context))) {
            definitionContext = service.getWadlContext();
            definitionContext.loadIfNecessary();
        } else {
            String def = PathUtils.expandPath(definition, service, context);
            if (definitionContext == null || !def.equals(wsdlContextDef)) {
                definitionContext = new WadlDefinitionContext(def);
                ((WadlDefinitionContext) definitionContext).load();
                ((WadlDefinitionContext) definitionContext).setInterface(service);
                wsdlContextDef = def;
            }
        }

        return definitionContext;
    }

    public boolean configure() {
        String definitionURL = definition;

        AbstractInterface<?> iface = (AbstractInterface<?>) getAssertable().getInterface();
        String orgDef = iface == null ? null : iface.getDefinition();

        if (StringUtils.isNullOrEmpty(definitionURL)) {
            definitionURL = orgDef;
        }

        definitionURL = UISupport
                .prompt("Specify definition url to validate by", "Configure Schema Compliance Assertion", definitionURL);

        if (definitionURL == null) {
            return false;
        }

        if (!canLoadDefinitionFrom(definitionURL)) {
            UISupport.showErrorMessage("No valid definition found in " + definitionURL + ". Only WSDL and WADL are supported");
            return false;
        }

        if (StringUtils.isNullOrEmpty(definitionURL) || definitionURL.equals(orgDef)) {
            definition = "";
        } else {
            definition = definitionURL;
        }

        setConfiguration(createConfiguration());
        return true;
    }

    private boolean canLoadDefinitionFrom(String definitionURL) {
        try {
            new WsdlContext(definitionURL).load();
            return true;
        } catch (Exception e) {
            try {
                new WadlDefinitionContext(definitionURL).load();
                return true;
            } catch (Exception e1) {
                return false;
            }
        }
    }

    protected XmlObject createConfiguration() {
        XmlObjectConfigurationBuilder builder = new XmlObjectConfigurationBuilder();
        return builder.add("definition", definition).finish();
    }

    protected String internalAssertRequest(MessageExchange messageExchange, SubmitContext context)
            throws AssertionException {
        WsdlContext wsdlContext;
        try {
            wsdlContext = (WsdlContext) getWsdlContext((WsdlMessageExchange) messageExchange, context);
        } catch (Exception e1) {
            throw new AssertionException(new AssertionError(e1.getMessage()));
        }
        WsdlValidator validator = new WsdlValidator(wsdlContext);

        try {
            AssertionError[] errors = validator.assertRequest((WsdlMessageExchange) messageExchange, false);
            if (errors.length > 0) {
                throw new AssertionException(errors);
            }
        } catch (AssertionException e) {
            throw e;
        } catch (Exception e) {
            throw new AssertionException(new AssertionError(e.getMessage()));
        }

        return "Schema compliance OK";
    }

    public static class Factory extends AbstractTestAssertionFactory {
        public Factory() {
            super(SchemaComplianceAssertion.ID, SchemaComplianceAssertion.LABEL, SchemaComplianceAssertion.class);
        }

        @Override
        public String getCategory() {
            return AssertionCategoryMapping.STATUS_CATEGORY;
        }

        @Override
        public boolean canAssert(Assertable assertable) {
            try {
                return super.canAssert(assertable) && assertable.getInterface() instanceof AbstractInterface
                        && ((AbstractInterface<?>) assertable.getInterface()).getDefinitionContext().hasSchemaTypes();
            } catch (Throwable e) {
                SoapUI.logError(e);
                return false;
            }
        }

        @Override
        public Class<? extends WsdlMessageAssertion> getAssertionClassType() {
            return SchemaComplianceAssertion.class;
        }

        @Override
        public AssertionListEntry getAssertionListEntry() {
            return new AssertionListEntry(SchemaComplianceAssertion.ID, SchemaComplianceAssertion.LABEL,
                    SchemaComplianceAssertion.DESCRIPTION);
        }
    }
}
